/*
 * Copyright (C) 2012-2013 Matthias Zeimer and the miniframes project group.
 * All rights reserved. 
 * 
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/EPL-1.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.miniframes.minivalues;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.junit.Test;

/**
 * <p>
 * Test-suite of {@link UnmodifiableArray}.
 * </p>
 * 
 * @author Matthias Zeimer
 * @version 4
 * @since 0.1, 20.01.2013
 */
public class UnmodifiableArrayTest {

    @Test
    public void testCreationAndUnmodifyablenessViaSourceReference() {
        final Integer[] source = new Integer[4];
        source[0] = 1;
        source[1] = 2;
        source[2] = 3;
        source[3] = 4;

        final UnmodifiableArray<Integer> out = UnmodifiableArray
                .valueOf(source);

        assertEquals(4, out.length());

        assertEquals(new Integer(1), out.componentAt(0));
        assertEquals(new Integer(2), out.componentAt(1));
        assertEquals(new Integer(3), out.componentAt(2));
        assertEquals(new Integer(4), out.componentAt(3));

        // Modify source
        source[0] = 4;
        source[1] = 3;
        source[2] = 2;
        source[3] = 1;

        // Change of source is NOT reflected in out
        assertEquals(new Integer(1), out.componentAt(0));
        assertEquals(new Integer(2), out.componentAt(1));
        assertEquals(new Integer(3), out.componentAt(2));
        assertEquals(new Integer(4), out.componentAt(3));
    }

    @Test
    public void testDataIsNotCopied() {
        final Object[] source = new Object[4];
        source[0] = new Object();
        source[1] = new Object();
        source[2] = new Object();
        source[3] = new Object();

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        assertEquals(4, out.length());

        assertSame(source[0], out.componentAt(0));
        assertSame(source[1], out.componentAt(1));
        assertSame(source[2], out.componentAt(2));
        assertSame(source[3], out.componentAt(3));
    }

    @Test
    public void testEquals_AtLeastOneNotEqualInstancesInBothArrays() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final String[] aNotEqualSource = new String[4];
        aNotEqualSource[0] = "A";
        aNotEqualSource[1] = "B";
        aNotEqualSource[2] = "X"; // Difference!
        aNotEqualSource[3] = "D";

        final UnmodifiableArray<String> notEqualToOut = UnmodifiableArray
                .valueOf(aNotEqualSource);

        assertFalse(out.equals(notEqualToOut));
    }

    @Test
    public void testEquals_DifferentCoponentTypes() {
        final String[] source = new String[4];
        source[0] = "1";
        source[1] = "2";
        source[2] = "3";
        source[3] = "4";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final Integer[] aSourceWithDifferentComponentTypeIsNotEqual = //
        new Integer[4];
        aSourceWithDifferentComponentTypeIsNotEqual[0] = 1;
        aSourceWithDifferentComponentTypeIsNotEqual[1] = 2;
        aSourceWithDifferentComponentTypeIsNotEqual[2] = 3;
        aSourceWithDifferentComponentTypeIsNotEqual[3] = 4;

        final UnmodifiableArray<Integer> notEqualToOut = UnmodifiableArray
                .valueOf(aSourceWithDifferentComponentTypeIsNotEqual);

        assertFalse(out.equals(notEqualToOut));
    }

    @Test
    public void testEquals_differentType() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertFalse(out.equals(new Object()));
    }

    @Test
    public void testEquals_EqualInstancesButInDifferentOrderInBothArrays() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final String[] aSourceWithDifferentOrderIsNotEqual = new String[4];
        aSourceWithDifferentOrderIsNotEqual[0] = "D";
        aSourceWithDifferentOrderIsNotEqual[1] = "C";
        aSourceWithDifferentOrderIsNotEqual[2] = "B";
        aSourceWithDifferentOrderIsNotEqual[3] = "A";

        final UnmodifiableArray<String> notEqualToOut = UnmodifiableArray
                .valueOf(aSourceWithDifferentOrderIsNotEqual);

        assertFalse(out.equals(notEqualToOut));
    }

    @Test
    public void testEquals_isConsistent() {
        final String[] sourceX = new String[4];
        sourceX[0] = "A";
        sourceX[1] = "B";
        sourceX[2] = "C";
        sourceX[3] = "D";

        final UnmodifiableArray<String> x = UnmodifiableArray.valueOf(sourceX);

        final String[] sourceY = new String[4];
        sourceY[0] = "A";
        sourceY[1] = "B";
        sourceY[2] = "C";
        sourceY[3] = "D";

        final UnmodifiableArray<String> y = UnmodifiableArray.valueOf(sourceY);

        assertTrue(x.equals(y));
        assertTrue(x.equals(y));
        assertTrue(x.equals(y));
    }

    @Test
    public void testEquals_isReflexive() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertEquals(out, out);
    }

    @Test
    public void testEquals_isSymmetric() {
        final String[] sourceX = new String[4];
        sourceX[0] = "A";
        sourceX[1] = "B";
        sourceX[2] = "C";
        sourceX[3] = "D";

        final UnmodifiableArray<String> x = UnmodifiableArray.valueOf(sourceX);

        final String[] sourceY = new String[4];
        sourceY[0] = "A";
        sourceY[1] = "B";
        sourceY[2] = "C";
        sourceY[3] = "D";

        final UnmodifiableArray<String> y = UnmodifiableArray.valueOf(sourceY);

        assertTrue(x.equals(y) && y.equals(x));
    }

    @Test
    public void testEquals_isTransitive() {
        final String[] sourceX = new String[4];
        sourceX[0] = "A";
        sourceX[1] = "B";
        sourceX[2] = "C";
        sourceX[3] = "D";

        final UnmodifiableArray<String> x = UnmodifiableArray.valueOf(sourceX);

        final String[] sourceY = new String[4];
        sourceY[0] = "A";
        sourceY[1] = "B";
        sourceY[2] = "C";
        sourceY[3] = "D";

        final UnmodifiableArray<String> y = UnmodifiableArray.valueOf(sourceY);

        final String[] sourceZ = new String[4];
        sourceZ[0] = "A";
        sourceZ[1] = "B";
        sourceZ[2] = "C";
        sourceZ[3] = "D";

        final UnmodifiableArray<String> z = UnmodifiableArray.valueOf(sourceZ);

        assertTrue(x.equals(y) && y.equals(z) && x.equals(z));
    }

    @Test
    public void testEquals_nullReferenceBehaviour() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertFalse(out.equals(null));
    }

    @Test
    public void testEqualsAndHashCode_EqualInstancesInBothArrays() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final String[] anEqualSource = new String[4];
        anEqualSource[0] = "A";
        anEqualSource[1] = "B";
        anEqualSource[2] = "C";
        anEqualSource[3] = "D";

        final UnmodifiableArray<String> equalToOut = UnmodifiableArray
                .valueOf(anEqualSource);

        assertEquals(out, equalToOut);
        assertEquals(out.hashCode(), equalToOut.hashCode());
    }

    @Test
    public void testEqualsAndHashCode_SameInstancesInBothArrays() {
        final Object[] source = new Object[4];
        source[0] = new Object();
        source[1] = new Object();
        source[2] = new Object();
        source[3] = new Object();

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);
        final UnmodifiableArray<Object> equalToOut = UnmodifiableArray
                .valueOf(source);

        assertEquals(out, equalToOut);
        assertEquals(out.hashCode(), equalToOut.hashCode());
    }

    @Test
    public void testExistenceOfADefaultConstructorForSerialisation()
            throws SecurityException, NoSuchMethodException,
            IllegalArgumentException, InstantiationException,
            IllegalAccessException, InvocationTargetException {

        @SuppressWarnings("rawtypes")
        final Class<UnmodifiableArray> clasz = UnmodifiableArray.class;

        @SuppressWarnings("rawtypes")
        final Constructor<UnmodifiableArray> defaultConstructor = clasz
                .getDeclaredConstructor();

        assertTrue(Modifier.isPrivate(defaultConstructor.getModifiers()));

        final boolean oldAccessibleValue = defaultConstructor.isAccessible();

        // Make default constructor accessible, create an instance and protect
        // it again:
        defaultConstructor.setAccessible(true);
        defaultConstructor.newInstance();
        defaultConstructor.setAccessible(oldAccessibleValue);
    }

    @Test
    public void testGetComponentType_Object() {
        final Object[] source = new Object[1];
        source[0] = new Object();

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        final Class<Object> componentType = out.getComponentType();
        assertSame(Object.class, componentType);
        assertSame(componentType, componentType);
    }

    @Test
    public void testGetComponentType_String() {
        final String[] source = new String[1];
        source[0] = "Test";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final Class<String> componentType = out.getComponentType();
        assertSame(String.class, componentType);
        assertSame(componentType, componentType);
    }

    @Test
    public void testHashCode_isConsistently() {
        final String[] source = new String[4];
        source[0] = "A";
        source[1] = "B";
        source[2] = "C";
        source[3] = "D";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        final int hashCode = out.hashCode();
        assertEquals(hashCode, out.hashCode());
        assertEquals(hashCode, out.hashCode());
        assertEquals(hashCode, out.hashCode());
    }

    @Test
    public void testIsEmpty_emptySourceArray() {
        final Object[] source = new Object[0];

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        assertTrue(out.isEmpty());
    }

    @Test
    public void testIsEmpty_nonEmptySourceArray() {
        final Object[] source = new Object[1];
        source[0] = new Object();

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        assertFalse(out.isEmpty());
    }

    @Test
    public void testIterator() {
        final Integer[] source = new Integer[4];
        source[0] = 1;
        source[1] = 2;
        source[2] = 3;
        source[3] = 4;

        final UnmodifiableArray<Integer> out = UnmodifiableArray
                .valueOf(source);
        final Iterator<Integer> outIterator = out.iterator();

        assertTrue(outIterator.hasNext());
        assertEquals(new Integer(1), outIterator.next());
        try {
            outIterator.remove();
            fail("UnsupportedOperationException expected");
        } catch (final UnsupportedOperationException expected) {
            // OK, this was expected.
        }

        assertTrue(outIterator.hasNext());
        assertEquals(new Integer(2), outIterator.next());
        try {
            outIterator.remove();
            fail("UnsupportedOperationException expected");
        } catch (final UnsupportedOperationException expected) {
            // OK, this was expected.
        }

        assertTrue(outIterator.hasNext());
        assertEquals(new Integer(3), outIterator.next());
        try {
            outIterator.remove();
            fail("UnsupportedOperationException expected");
        } catch (final UnsupportedOperationException expected) {
            // OK, this was expected.
        }

        assertTrue(outIterator.hasNext());
        assertEquals(new Integer(4), outIterator.next());
        try {
            outIterator.remove();
            fail("UnsupportedOperationException expected");
        } catch (final UnsupportedOperationException expected) {
            // OK, this was expected.
        }

        assertFalse(outIterator.hasNext());
        try {
            outIterator.next();
            fail("NoSuchElementException expected");
        } catch (final NoSuchElementException expected) {
            // OK, this was expected.
        }

        // Subsequent call creates new iterator:
        assertNotSame(outIterator, out.iterator());
    }

    @Test
    public void testOutOfBounds() {
        final Integer[] source = new Integer[2];
        source[0] = 1;
        source[1] = 2;

        final UnmodifiableArray<Integer> out = UnmodifiableArray
                .valueOf(source);

        assertEquals(2, out.length());

        assertEquals(new Integer(1), out.componentAt(0));
        assertEquals(new Integer(2), out.componentAt(1));

        try {
            out.componentAt(3);
            fail("ArrayIndexOutOfBoundsException expected");
        } catch (final ArrayIndexOutOfBoundsException expected) {
            // OK, this was expected.
        }
    }

    @Test
    public void testSerialisation() throws IOException, ClassNotFoundException {
        final Integer[] source = new Integer[4];
        source[0] = 1;
        source[1] = 2;
        source[2] = 3;
        source[3] = 4;

        final UnmodifiableArray<Integer> out = UnmodifiableArray
                .valueOf(source);

        assertEquals(4, out.length());
        assertEquals(Integer.class, out.getComponentType());
        assertEquals((Integer) 1, out.componentAt(0));
        assertEquals((Integer) 2, out.componentAt(1));
        assertEquals((Integer) 3, out.componentAt(2));
        assertEquals((Integer) 4, out.componentAt(3));

        // Serialise:
        final ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutput = new ObjectOutputStream(
                byteOutput);
        objectOutput.writeObject(out);

        // The serialised version:
        final byte[] serialisedVersion = byteOutput.toByteArray();

        // De-serialise:
        final ByteArrayInputStream byteInput = new ByteArrayInputStream(
                serialisedVersion);
        final ObjectInputStream objectInput = new ObjectInputStream(byteInput);
        final Object deserialisedObject = objectInput.readObject();

        // Verify the deserialised version is equal to out:
        assertTrue(deserialisedObject instanceof UnmodifiableArray);

        @SuppressWarnings("unchecked")
        final UnmodifiableArray<Integer> deserialisedVersion = //
        (UnmodifiableArray<Integer>) deserialisedObject;

        assertNotSame(out, deserialisedVersion);

        assertEquals(4, deserialisedVersion.length());
        assertEquals(Integer.class, deserialisedVersion.getComponentType());
        assertEquals((Integer) 1, deserialisedVersion.componentAt(0));
        assertEquals((Integer) 2, deserialisedVersion.componentAt(1));
        assertEquals((Integer) 3, deserialisedVersion.componentAt(2));
        assertEquals((Integer) 4, deserialisedVersion.componentAt(3));
    }

    @Test
    public void testToArray() {
        final Object[] source = new Object[4];
        source[0] = new Object();
        source[1] = new Object();
        source[2] = new Object();
        source[3] = new Object();

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        final Object[] outAsArray = out.toArray();

        assertEquals(4, outAsArray.length);

        assertSame(source[0], outAsArray[0]);
        assertSame(source[1], outAsArray[1]);
        assertSame(source[2], outAsArray[2]);
        assertSame(source[3], outAsArray[3]);

        // Change to returned array will not affect out:
        outAsArray[0] = new Object();

        assertSame(source[0], out.componentAt(0));
        assertSame(source[1], out.componentAt(1));
        assertSame(source[2], out.componentAt(2));
        assertSame(source[3], out.componentAt(3));

        // subsequent call will return new array:
        assertNotSame(outAsArray, out.toArray());
    }

    @Test
    public void testToString_EmptyArray() {
        final Object[] source = new Object[0];

        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf(source);

        assertEquals("[]", out.toString());
    }

    @Test
    public void testToString_EmptyArrayImmediateCall() {
        final UnmodifiableArray<Object> out = UnmodifiableArray.valueOf();

        assertEquals("[]", out.toString());
    }

    @Test
    public void testToString_MixedNumbersArray() {
        final Number[] source = new Number[5];
        source[0] = 1;
        source[1] = 2l;
        source[2] = 3.9;
        source[3] = BigInteger.valueOf(4);
        source[4] = BigDecimal.valueOf(58, 2);

        final UnmodifiableArray<Number> out = UnmodifiableArray.valueOf(source);

        assertEquals("[1, 2, 3.9, 4, 0.58]", out.toString());
    }

    @Test
    public void testToString_NullAsOnlyCompoent() {
        final String[] source = new String[1];
        source[0] = null;

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertEquals("[null]", out.toString());
    }

    @Test
    public void testToString_NullCompoent() {
        final String[] source = new String[3];
        source[0] = "one";
        source[1] = null;
        source[2] = "three";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertEquals("[one, null, three]", out.toString());
    }

    @Test
    public void testToString_OneComponent() {
        final String[] source = new String[1];
        source[0] = "one";

        final UnmodifiableArray<String> out = UnmodifiableArray.valueOf(source);

        assertEquals("[one]", out.toString());
    }

    @Test(expected = NullPointerException.class)
    public void testValueOfDoesNotAcceptNull() {
        final Object[] nullArray = null;
        UnmodifiableArray.valueOf(nullArray);
    }

    //

//    @Test
//    public void testStrPool() {
//        String s;
//        MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
//        for (int i = 0; i < 10000000; i++) {
//            s = ("Hallo" + i).intern();
//            s = null;
//            System.gc();
//            Thread.yield();
//            if (i % 100 == 0) {
//                long used = mbean.getNonHeapMemoryUsage().getUsed();
//                Thread.yield();
//            }
//            /*- results in
//             * 
//             * 7129128
//             * 7180656
//             * 7180656
//             * 7180656
//             * 7180656
//             * 7180656
//             * ...
//             * 
//             */
//        }
//    }
}
